﻿#region Copyright(c) 2006 ZO, All right reserved.
// -----------------------------------------------------------------------------
// Copyright(c) 2006 ZO, All right reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//   1.  Redistribution of source code must retain the above copyright notice,
//       this list of conditions and the following disclaimer.
//   2.  Redistribution in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//   3.  The name of the author may not be used to endorse or promote products
//       derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// -----------------------------------------------------------------------------
#endregion


#region Using directives

using System;
using System.Security.Cryptography;
using ZO.SmartCore.Core;
using ZO.SmartCore.Helpers;
using ZO.SmartCore.Security.Cryptography.Properties;
using ArgumentException= ZO.SmartCore.Core.ArgumentException;
using ArgumentNullException= ZO.SmartCore.Core.ArgumentNullException;

#endregion

namespace ZO.SmartCore.Security.Cryptography
{
    /// <summary>
    /// Represents basic cryptography services for a HashAlgorithm. 
    /// </summary>
    public sealed class HashCryptographer : DisposableObject
    {

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="HashCryptographer"/> class.
        /// </summary>
        public HashCryptographer()
        {
            this.hashAlgorithm = GetAlgorithm(this.Algorithm, null);

        } // HashCryptographer

        /// <overloads>
        ///     Initializes a new instance of the <see cref="SymmetricCryptographer"/> class.
        /// </overloads>
        /// <summary>
        ///     Initializes a new instance of the <see cref="SymmetricCryptographer"/> class.
        /// </summary>
        /// <param name="algorithm"><see cref="T:SmartCore.Security.Cryptography.HashCryptographer"/>
        /// 	<para>
        /// algorithm type
        /// </para></param>
        public HashCryptographer(HashAlgorithm algorithm)
        {
            this._Algorithm = algorithm;
            this.hashAlgorithm = GetAlgorithm(this.Algorithm, null);
        } // SymmetricCryptographer


        /// <summary>
        /// Initializes a new instance of the <see cref="HashCryptographer"/> class with algorithm type and secret key.
        /// </summary>
        /// <param name="algorithm"><see cref="T:SmartCore.Security.Cryptography.HashCryptographer"/>
        /// 	<para>
        /// algorithm type
        /// </para></param>
        /// <param name="key"><see cref="T:System.Array"/>
        /// 	<para>
        /// secret key for the symmetric algorithm
        /// </para></param>
        public HashCryptographer(HashAlgorithm algorithm, byte[] key)
        {
            if (key == null) throw new ArgumentNullException("key");
            this._Algorithm = algorithm;
            this.key = key;
            this.hashAlgorithm = GetAlgorithm(this.Algorithm, key);
        } // HashCryptographer
        #endregion

        #region Destructor
        /// <summary>
        /// Releases unmanaged resources and performs other cleanup operations before the
        /// <see cref="T:SmartCore.Security.Cryptography.HashCryptographer"/> is reclaimed by garbage collection.
        /// </summary>
        ~HashCryptographer()
        {
            this.Dispose();
        } // ~HashCryptographer

        #endregion

        #region Fields
        /// <summary>
        ///     Hash Encrypter
        /// </summary>
        private System.Security.Cryptography.HashAlgorithm hashAlgorithm;


        /// <summary>
        ///     Hash Algorithm Algorithm Type
        /// </summary>
        private HashAlgorithm _Algorithm = HashAlgorithm.MD5;
        /// <summary>
        /// Defines the salt length used by the provider.
        /// </summary>
        private const int SaltLength = 0x10;

        private bool _UseSalt;


        /// <summary>
        ///     Protected Key for the symmetric algorithm
        /// </summary>
        private byte[] key;
        #endregion

        #region Events

        #endregion

        #region Operators

        #endregion

        #region Properties
        /// <summary>
        ///     Get or Set the Symmetric Algorithm Type
        /// </summary>
        /// 
        /// <value>
        ///     <see cref="T:SmartCore.Security.Cryptography.SymmetricAlgorithm"/>
        /// 	<para>
        /// 	      
        /// 	 </para>
        /// </value>
        public HashAlgorithm Algorithm
        {
            get { return _Algorithm; } // get
            set
            {
                if (this._Algorithm != value)
                {
                    this._Algorithm = value;
                    this.hashAlgorithm = GetAlgorithm(value, key);

                } // if
            } // set
        } // Algorithm

        /// <summary>
        /// Create Hash with Salt
        /// </summary>
        public bool UseSalt
        {
            get
            {
                return _UseSalt;
            } // get
            set
            {
                this._UseSalt = value;
            } // set
        } // UseSalt
        #endregion

        #region Methods


        private static byte[] GetSalt()
        {
            return ByteHelper.GetRandomBytes(SaltLength);
        } // GetSalt

        /// <summary>
        /// Get Symmetric Alogoritm upon current type
        /// </summary>
        /// <param name="algorithmType"><see cref="T:System.String"/>
        /// 	<para>
        /// Symmetric Alogoritm Type
        /// </para></param>
        /// <param name="key">The key.</param>
        /// <returns>
        /// 	<see cref="T:System.Security.Cryptography.SymmetricAlgorithm"/>
        /// 	<para>
        /// SymmetricAlgorithm object
        /// </para>
        /// </returns>
        private static System.Security.Cryptography.HashAlgorithm GetAlgorithm(HashAlgorithm algorithmType, byte[] key)
        {
            System.Security.Cryptography.HashAlgorithm hashFunction;
            try
            {
                Type type = Type.GetType(ReflectionHelper.GetDescription(algorithmType), true);
                hashFunction = Activator.CreateInstance(type) as System.Security.Cryptography.HashAlgorithm;

                KeyedHashAlgorithm keyedHashAlgorithm = hashFunction as KeyedHashAlgorithm;
                if ((null != keyedHashAlgorithm) && (key != null))
                {
                    keyedHashAlgorithm.Key = key;
                }

            } // try
            catch (Exception ex)
            {
                // We want to supress any type of exception here for security reasons.
                throw new CryptographicException(Resources.ExceptionCreatingAlgorithmInstance, ex);
            } // catch

            if (hashFunction == null)
            {
                throw new CryptographicException(Resources.ExceptionMustBeAHashAlgorithm);
            } // if

            return hashFunction;
        } // GetHashAlgorithm


        /// <summary>
        /// Override This Method To Dispose Unmanaged Resources.
        /// </summary>
        protected override void DisposeManagedResources()
        {
            if (hashAlgorithm != null)
            {
                hashAlgorithm.Clear();
                hashAlgorithm = null;
            }
        }

        /// <summary>
        /// <para>Computes the hash value of the plaintext.</para>
        /// </summary>
        /// <param name="plaintext"><para>The plaintext in which you wish to hash.</para></param>
        /// <returns><para>The resulting hash.</para></returns>
        public byte[] ComputeHash(byte[] plaintext)
        {
            return CreateHashWithSalt(plaintext, null);
        }




        /// <summary>
        /// Creates a hash with a specified salt.
        /// </summary>
        /// <param name="plaintext">The plaintext to hash.</param>
        /// <param name="salt">The hash salt.</param>
        /// <returns>The computed hash.</returns>
        private byte[] CreateHashWithSalt(byte[] plaintext, byte[] salt)
        {
            AddSaltToPlainText(ref salt, ref plaintext);

            byte[] hash = hashAlgorithm.ComputeHash(plaintext);
            AddSaltToHash(salt, ref hash);
            return hash;
        }

        private void AddSaltToPlainText(ref byte[] salt, ref byte[] plaintext)
        {
            if (this.UseSalt)
            {
                if (salt == null)
                {
                    salt = GetSalt();
                }
                plaintext = ByteHelper.Combine(salt, plaintext);
            }
        }

        private void AddSaltToHash(byte[] salt, ref byte[] hash)
        {
            if (this.UseSalt)
            {

                hash = ByteHelper.Combine(salt, hash);
            } // if
        }

        /// <summary>
        /// Compares plain text input with a computed hash.
        /// </summary>
        /// <param name="plainText">The input for which you want to compare the hash to.</param>
        /// <param name="hashedText">The hash value for which you want to compare the input to.</param>
        /// <returns><c>true</c> if plainText hashed is equal to the hashedText. Otherwise, <c>false</c>.</returns>
        public bool CompareHash(byte[] plainText, byte[] hashedText)
        {
            if (plainText == null) throw new ArgumentNullException("plainText");
            if (hashedText == null) throw new ArgumentNullException("hashedText");
            if (hashedText.Length == 0) throw new ArgumentException(Resources.ExceptionByteArrayValueMustBeGreaterThanZeroBytes, "hashedText");

            bool result;

            byte[] hashedPlainText;
            byte[] salt = null;

            try
            {
                try
                {
                    salt = ExtractSalt(hashedText);
                    hashedPlainText = CreateHashWithSalt(plainText, salt);
                }
                finally
                {
                    ByteHelper.ZeroOutBytes(salt);

                }

                result = ByteHelper.Compare(hashedPlainText, hashedText);
            }
            catch
            {
                throw;
            }

            return result;
        }

        /// <summary>
        /// Extracts the salt from the hashedText.
        /// </summary>
        /// <param name="hashedtext">The hash in which to extract the salt.</param>
        /// <returns>The extracted salt.</returns>
        private byte[] ExtractSalt(byte[] hashedtext)
        {
            if (!this.UseSalt)
            {
                return null;
            }

            byte[] salt = null;
            if (hashedtext.Length > SaltLength)
            {
                salt = new byte[SaltLength];
                Buffer.BlockCopy(hashedtext, 0, salt, 0, SaltLength);
            }
            return salt;
        }
  
        #endregion
    }
}
